/*
 * Ethan Levine
 * 
 * The contents of this file are subject to the GNU General Public
 * License (GPL) v2.  The full contents of this license can be
 * found at Mathma's website, http://www.codeplex.com/mathma/license
 */

package mathma.core.real;

import mathma.core.expr.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;

/**
 * @since 0.1.0
 * @author Ethan Levine
 */
public class FiniteReal extends Real {
    
    private static final int EXP_ITERATIONS = 100;
    private static final double EXP_MAX_THRESHOLD = 1;
    private static final int SINE_ITERATIONS = 10;
    private static final int LN_ITERATIONS = 10;
    private static final int INV_SINE_ITERATIONS = 10;
    private static final int INV_TANGENT_ITERATIONS = 10;
    
    private BigDecimal value;
    
    public FiniteReal(int v) {
        value = new BigDecimal(v, MathContext.UNLIMITED);
    }
    
    public FiniteReal(double v) {
        value = new BigDecimal(v, MathContext.UNLIMITED);
    }

    public FiniteReal(float v) {
        value = new BigDecimal(v, MathContext.UNLIMITED);
    }
    
    public FiniteReal(long v) {
        value = new BigDecimal(v, MathContext.UNLIMITED);
    }
    
    public FiniteReal(short v) {
        value = new BigDecimal(v, MathContext.UNLIMITED);
    }
    
    public FiniteReal(Number v) {
        if (v.getClass().equals(BigDecimal.class)) {
            value = (BigDecimal) v;
        } else if (v.getClass().equals(BigInteger.class)) {
            value = new BigDecimal((BigInteger) v, MathContext.UNLIMITED);
        } else if (v.getClass().equals(Double.class) ||
                v.getClass().equals(Float.class)) {
            value = new BigDecimal(v.doubleValue(), MathContext.UNLIMITED);
        } else {
            value = new BigDecimal(v.longValue(), MathContext.UNLIMITED);
        }
    }

    public FiniteReal(Real v) {
        value = v.bigDecimalValue();
    }
    
    public boolean equals(Object other) {
        if (other.getClass().equals(Double.class) ||
                other.getClass().equals(Float.class)) {
            return value.compareTo(new BigDecimal(
                    ((Number) other).doubleValue())) == 0;
        } else if (other.getClass().equals(Long.class) ||
                other.getClass().equals(Integer.class) ||
                other.getClass().equals(Short.class) ||
                other.getClass().equals(Byte.class)) {
            return value.compareTo(new BigDecimal(
                    ((Number) other).longValue())) == 0;
        } else if (other.getClass().equals(IndeterminateReal.class) ||
                other.getClass().equals(InfiniteReal.class)) {
            return false;
        } else if (other.getClass().equals(FiniteReal.class)) {
            return value.compareTo(((Real) other).bigDecimalValue()) == 0;
        } else if (other.getClass().equals(BigDecimal.class)) {
            return value.compareTo((BigDecimal) other) == 0;
        } else if (other.getClass().equals(BigInteger.class)) {
            return value.compareTo(new BigDecimal((BigInteger) other)) == 0;
        } else {
            return false;
        }
    }
    
    public boolean isPositive() {
        return value.compareTo(BigDecimal.ZERO) > 0;
    }
    
    public boolean isNegative() {
        return value.compareTo(BigDecimal.ZERO) < 0;
    }
    
    public boolean isInteger() {
        return value.remainder(BigDecimal.ONE).compareTo(BigDecimal.ZERO) == 0;
    }
    
    public boolean isFinite() {
        return true;
    }
    
    public boolean isInfinite() {
        return false;
    }
    
    public boolean isIndeterminate() {
        return false;
    }
    
    public boolean isLessThan(Real other) {
        if (other.isIndeterminate()) {
            return false;
        } else if (other.isInfinite()) {
            return other.isPositive();
        } else if (other.isFinite()) {
            return value.compareTo(other.bigDecimalValue()) == -1;
        } else {
            return false;
        }
    }
    
    public boolean isGreaterThan(Real other) {
        if (other.isIndeterminate()) {
            return false;
        } else if (other.isInfinite()) {
            return other.isNegative();
        } else if (other.isFinite()) {
            return value.compareTo(other.bigDecimalValue()) == 1;
        } else {
            return false;
        }
    }

    public int intValue() {
        return value.intValue();
    }

    public double doubleValue() {
        return value.doubleValue();
    }

    public long longValue() {
        return value.longValue();
    }

    public short shortValue() {
        return value.shortValue();
    }

    public byte byteValue() {
        return value.byteValue();
    }

    public float floatValue() {
        return value.floatValue();
    }

    public BigDecimal bigDecimalValue() {
        return value;
    }

    public Real negate() {
        return new FiniteReal(value.negate(MathContext.UNLIMITED));
    }

    public Real absoluteValue() {
        return new FiniteReal(value.abs(MathContext.UNLIMITED));
    }
    
    public Real floor() {
        if (isInteger()) {
            return this;
        } else if (isNegative()) {
            return new FiniteReal(value.divideToIntegralValue(BigDecimal.ONE)).subtract(Real.ONE);
        } else {
            return new FiniteReal(value.divideToIntegralValue(BigDecimal.ONE));
        }
    }
    
    public Real ceiling() {
        if (isInteger()) {
            return this;
        } else {
            return floor().add(Real.ONE);
        }
    }
    
    public Real truncate() {
        if (!isNegative()) {
            return floor();
        } else {
            return ceiling();
        }
    }

    public Real round() {
        if (new FiniteReal(.5).isGreaterThan(modulo(Real.ONE))) {
            return floor();
        } else {
            return ceiling();
        }
    }
    
    public Real add(Real operand) {
        if (operand.isIndeterminate()) {
            return IndeterminateReal.INDETERMINATE_OPERATOR;
        } else if (operand.isInfinite()) {
            return operand;
        } else if (operand.isFinite()) {
            return new FiniteReal(value.add(operand.bigDecimalValue(),
                    MathContext.UNLIMITED));
        } else {
            return operand.add(this);
        }
    }
    
    public Real subtract(Real operand) {
        if (operand.isIndeterminate()) {
            return IndeterminateReal.INDETERMINATE_OPERATOR;
        } else if (operand.isInfinite()) {
            return operand.negate();
        } else {
            return new FiniteReal(value.subtract(operand.bigDecimalValue(),
                    MathContext.UNLIMITED));
        }
    }
    
    public Real multiply(Real operand) {
        if (operand.isIndeterminate()) {
            return IndeterminateReal.INDETERMINATE_OPERATOR;
        } else if (operand.isInfinite()) {
            return operand.multiply(this);
        } else {
            return new FiniteReal(value.multiply(operand.bigDecimalValue(),
                    MathContext.UNLIMITED));
        }
    }
    
    public Real divideBy(Real denominator) {
        if (denominator.isIndeterminate()) {
            return IndeterminateReal.INDETERMINATE_OPERATOR;
        } else if (denominator.isInfinite()) {
            return Real.ZERO;
        } else {
            try {
                return new FiniteReal(value.divide(denominator.bigDecimalValue(),
                        MathContext.UNLIMITED));
            } catch (ArithmeticException e) {
                return new FiniteReal(value.divide(denominator.bigDecimalValue(),
                        MathContext.DECIMAL128));
            }
        }
    }
    
    public Real modulo(Real modulus) {
        if (modulus.isIndeterminate()) {
            return IndeterminateReal.INDETERMINATE_OPERATOR;
        } else if (modulus.isInfinite()) {
            // You end up with infinity * 0, in this case.
            // This function should really return a positive number, however.
            // Thus, if this is negative, then return +inf.
            // If modulus is -inf, return the negation of the normal operation.
            if (modulus.isPositive()) {
                // Include 0 as positive...
                if (!isNegative()) {
                    return this;
                } else {
                    return InfiniteReal.POSITIVE_INFINITY;
                }
            } else {
                if (!isNegative()) {
                    return this.negate();
                } else {
                    return InfiniteReal.NEGATIVE_INFINITY;
                }
            }
        } else {
            Real q;
            if (modulus.isPositive()) {
                q = new FiniteReal(value.divide(modulus.bigDecimalValue(),
                        MathContext.DECIMAL128)).floor();
            } else {
                q = new FiniteReal(value.divide(modulus.bigDecimalValue(),
                        MathContext.DECIMAL128)).ceiling();
            }
            return subtract(modulus.multiply(q));
        }
    }
    
    public Real sine() {
        // Assumes RADIANS.
        Real truncated = modulo(RealConstant.PI.evaluate());
        Real running = Real.ZERO;
        Real runningX = truncated;
        boolean n = true;
        for (int x = 0; x < SINE_ITERATIONS; x++) {
            if (n) {
                running.add(runningX.divideBy(new FiniteReal(x * 2 + 1).factorial()));
            } else {
                running.subtract(runningX.divideBy(new FiniteReal(x * 2 + 1).factorial()));
            }
            runningX = runningX.multiply(truncated).multiply(truncated);
            n = !n;
        }
        return running;
    }
    
    public Real cosine() {
        // Assumes RADIANS.
        return RealConstant.PI.evaluate().divideBy(Real.TWO).subtract(this);
    }
    
    public Real tangent() {
        // Assumes RAIDANS.
        return sine().divideBy(cosine());
    }
    
    public Real secant() {
        return Real.ONE.divideBy(cosine());
    }
    
    public Real cosecant() {
        return Real.ONE.divideBy(sine());
    }
    
    public Real cotangent() {
        return cosine().divideBy(sine());
    }
    
    public Real invSine() {
        Real running = Real.ZERO;
        Real runningCoef = Real.ONE;
        Real runningExp = this;
        for (int x = 0; x < INV_SINE_ITERATIONS; x++) {
            running = running.add(runningCoef.multiply(runningExp).divideBy(new FiniteReal(x * 2 + 1)));
            runningCoef = runningCoef.multiply(new FiniteReal(2 * x - 1)).divideBy(new FiniteReal(2 * x));
            runningExp = runningExp.multiply(this).multiply(this);
        }
        return running;
    }
    
    public Real invCosine() {
        return RealConstant.PI.evaluate().divideBy(Real.TWO).subtract(invSine());
    }
    
    public Real invTangent() {
        Real running = Real.ZERO;
        Real runningExp = this;
        boolean n = true;
        for (int x = 0; x < INV_TANGENT_ITERATIONS; x++) {
            if (n) {
                running = running.add(runningExp.divideBy(new FiniteReal(2 * x + 1)));
            } else {
                running = running.subtract(runningExp.divideBy(new FiniteReal(2 * x + 1)));
            }
            runningExp = runningExp.multiply(this).multiply(this);
            n = !n;
        }
        return running;
    }
    
    public Real invSecant() {
        return Real.ZERO.divideBy(this).invCosine();
    }
    
    public Real invCosecant() {
        return Real.ZERO.divideBy(this).invSine();
    }

    public Real invCotangent() {
        return RealConstant.PI.evaluate().divideBy(Real.TWO).subtract(invTangent());
    }
    
    public Real hyperSine() {
        return exp().subtract(negate().exp()).divideBy(Real.TWO);
    }
    
    public Real hyperCosine() {
        return exp().add(negate().exp()).divideBy(Real.TWO);
    }
    
    public Real hyperTangent() {
        return hyperSine().divideBy(hyperCosine());
    }
    
    public Real hyperSecant() {
        return Real.ONE.divideBy(hyperCosine());
    }
    
    public Real hyperCosecant() {
        return Real.ONE.divideBy(hyperSine());
    }
    
    public Real hyperCotangent() {
        return hyperCosine().divideBy(hyperSine());
    }
    
    public Real invHyperSine() {
        return add(multiply(this).add(Real.ONE).sqrt()).ln();
    }
    
    public Real invHyperCosine() {
        return add(subtract(Real.ONE).sqrt().multiply(add(
                Real.ONE).sqrt())).ln();
    }
    
    public Real invHyperTangent() {
        return add(Real.ONE).divideBy(Real.ONE.subtract(
                this)).ln().divideBy(Real.TWO);
    }
    
    public Real invHyperSecant() {
        return Real.ONE.divideBy(this).subtract(
                Real.ONE).sqrt().multiply(Real.ONE.divideBy(
                this).add(Real.ONE).sqrt()).add(new FiniteReal(
                1).divideBy(this)).ln();
    }
    
    public Real invHyperCosecant() {
        return Real.ONE.divideBy(multiply(this)).add(new FiniteReal(
                1)).sqrt().add(Real.ONE.divideBy(this)).ln();
    }
    
    public Real invHyperCotangent() {
        return add(Real.ONE).divideBy(subtract(new FiniteReal(
                1))).ln().divideBy(Real.TWO);
    }

    public Real factorial() {
        // Only works on integers, now.
        if (isInteger()) {
            if (isPositive()) {
                Real running = Real.ONE;
                for (int x = intValue(); x > 0; x--) {
                    running = running.multiply(new FiniteReal(x));
                }
                return running;
            } else if (isNegative()) {
                // if value is odd, then return infinity
                // otherwise, return negative infinity
                if (modulo(Real.TWO).equals(0)) {
                    return InfiniteReal.NEGATIVE_INFINITY;
                } else {
                    return InfiniteReal.POSITIVE_INFINITY;
                }
            } else {
                return Real.ONE;
            }
        } else {
            throw new UnknownValueException("factorial() only works on integers.");
        }
    }

    public Real exp() {
        if (isGreaterThan(new FiniteReal(EXP_MAX_THRESHOLD))) {
            Real v = divideBy(Real.TWO).exp();
            return v.multiply(v);
        } else if (isLessThan(new FiniteReal(EXP_MAX_THRESHOLD).negate())) {
            Real v = divideBy(Real.TWO).exp();
            return v.multiply(v);
        } else {
            Real running = new FiniteReal(EXP_ITERATIONS * 4 + 6);
            Real val_sq = multiply(this);
            for (int x = EXP_ITERATIONS; x > 0; x--) {
                running = new FiniteReal(x * 4 + 2).add(val_sq.divideBy(running));
            }
            running = Real.TWO.subtract(this).add(val_sq.divideBy(running));
            running = Real.ONE.add(multiply(Real.TWO).divideBy(running));
            return running;
        }
    }

    public Real ln() {
        Real val_minus_1 = subtract(Real.ONE);
        Real runningExp = val_minus_1;
        Real running = Real.ZERO;
        boolean n = true;
        for (int x = 1; x <= LN_ITERATIONS; x++) {
            if (n) {
                running = running.add(runningExp.divideBy(new FiniteReal(x)));
            } else {
                running = running.subtract(runningExp.divideBy(new FiniteReal(x)));
            }
            runningExp = runningExp.multiply(val_minus_1);
            n = !n;
        }
        return running;
    }
    
    public Real sqrt() {
        return ln().divideBy(Real.TWO).exp();
    }
    
    public Real power(Real exponent) {
        /* PATTERN:
         * 
         * e ^ (exponent * ln(base))
         */
        if (exponent.isIndeterminate()) {
            return IndeterminateReal.INDETERMINATE_OPERATOR;
        } else {
            return ln().multiply(exponent).exp();
        }
    }
}
